/*
 * $Id: authen.c,v 1.13 2009-04-10 18:46:43 heas Exp $
 *
 * Copyright (c) 1995-1998 by Cisco systems, Inc.

 * Permission to use, copy, modify, and distribute this software for
 * any purpose and without fee is hereby granted, provided that this
 * copyright and permission notice appear on all copies of the
 * software and supporting documentation, the name of Cisco Systems,
 * Inc. not be used in advertising or publicity pertaining to
 * distribution of the program without specific prior permission, and
 * notice be given in supporting documentation that modification,
 * copying and distribution is by permission of Cisco Systems, Inc.

 * Cisco Systems, Inc. makes no representations about the suitability
 * of this software for any purpose.  THIS SOFTWARE IS PROVIDED ``AS
 * IS'' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
 * WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE.
 */

#include "tac_plus.h"

static int choose();
static void authenticate();
static void do_start();

/*
 *  Come here when we receive an authentication START packet
 */
void authen(u_char *pak)
{
	char msg[55];
	struct authen_start *start;
	HDR *hdr;

	hdr = (HDR *) pak;
	start = (struct authen_start *) (pak + TAC_PLUS_HDR_SIZE);

	/* Must be at least sizeof(struct authen_start) in size */
	if (ntohl(hdr->datalength) < TAC_AUTHEN_START_FIXED_FIELDS_SIZE) {
		report(LOG_ERR, "%s: authen minimum payload length: %zu, got: %u", session.peer, TAC_AUTHEN_START_FIXED_FIELDS_SIZE,
				ntohl(hdr->datalength));
		send_authen_error("Invalid AUTHEN/START packet (too short)");
		return;
	}

	if ((hdr->seq_no != 1)
			|| (ntohl(hdr->datalength)
			!= TAC_AUTHEN_START_FIXED_FIELDS_SIZE + start->user_len + start->port_len + start->rem_addr_len
			+ start->data_len)) {
		send_authen_error("Invalid AUTHEN/START packet (check keys)");
		return;
	}

	switch (start->action) {
		case TAC_PLUS_AUTHEN_LOGIN:
		case TAC_PLUS_AUTHEN_SENDAUTH:
		case TAC_PLUS_AUTHEN_SENDPASS:
			do_start(pak);
			return;
		default:
			snprintf(msg, sizeof (msg), "Invalid AUTHEN/START action=%d", start->action);
			send_authen_error(msg);
			return;
	}
}

/*
 * We have a valid AUTHEN/START packet. Fill out data structures and
 * attempt to authenticate.
 */
static void do_start(u_char *pak)
{
	struct identity identity;
	struct authen_data authen_data;
	struct authen_type authen_type;
	struct authen_start *start;
	u_char *p;
	int ret;

	if (debug & DEBUG_PACKET_FLAG)
		report(LOG_DEBUG, "Authen Start request");

	/* fixed fields of this packet */
	start = (struct authen_start *) (pak + TAC_PLUS_HDR_SIZE);

	/* variable length data starts here */
	p = pak + TAC_PLUS_HDR_SIZE + TAC_AUTHEN_START_FIXED_FIELDS_SIZE;

	/* The identity structure */

	/* zero out identity struct so that all strings can be NULL terminated */
	memset(&identity, 0, sizeof (struct identity));

	identity.username = tac_make_string(p, (int) start->user_len);
	p += start->user_len;

	identity.NAS_name = tac_strdup(session.peer);
#ifdef ACLS
	identity.NAS_ip = tac_strdup(session.peerip);
#endif

	identity.NAS_port = tac_make_string(p, (int) start->port_len);
	p += start->port_len;

	if (start->port_len <= 0) {
		strcpy(session.port, "unknown-port");
	} else {
		strcpy(session.port, identity.NAS_port);
	}

	identity.NAC_address = tac_make_string(p, (int) start->rem_addr_len);
	p += start->rem_addr_len;

	identity.priv_lvl = start->priv_lvl;

	/* The authen_data structure */
	memset(&authen_data, 0, sizeof (struct authen_data));

	authen_data.NAS_id = &identity;
	authen_data.action = start->action;
	authen_data.service = start->service;
	authen_data.type = start->authen_type;
	authen_data.client_dlen = start->data_len;

	authen_data.client_data = tac_malloc(start->data_len);
	memcpy(authen_data.client_data, p, start->data_len);

	/* The authen_type structure */
	memset(&authen_type, 0, sizeof (struct authen_type));

	authen_type.authen_type = start->authen_type;

	/*
	 * All data structures are now initialised. Now see if we can authenticate
	 * this puppy. Begin by choosing a suitable authentication function to
	 * call to actually do the work.
	 */
	ret = choose(&authen_data, &authen_type);

	switch (ret) {
		case 1:
			/* A successful choice. Authenticate */
			authenticate(&authen_data, &authen_type);
			break;
		case 0:
			/* We lost our connection, aborted, or something dreadful happened */
			break;
	}

	/* free data structures */
	if (authen_data.server_msg) {
		free(authen_data.server_msg);
		authen_data.server_msg = NULL;
	}
	if (authen_data.server_data) {
		free(authen_data.server_data);
		authen_data.server_data = NULL;
	}
	if (authen_data.client_msg) {
		free(authen_data.client_msg);
		authen_data.client_msg = NULL;
	}
	if (authen_data.client_data) {
		free(authen_data.client_data);
		authen_data.client_data = NULL;
	}
	if (authen_data.method_data) {
		report(LOG_ERR, "%s: Method data not set to NULL after authentication", session.peer);
	}
	free(identity.username);
	free(identity.NAS_name);
	free(identity.NAS_port);
	free(identity.NAC_address);
	return;
}

/*
 * Choose an authentication function. Return 1 if we successfully
 * chose a function.  0 if we couldn't make a choice for some reason
 */
static int choose(struct authen_data *datap, struct authen_type *typep)
{
	int iterations = 0;
	int status;
	char *prompt;
	struct authen_cont *cont;
	u_char *reply;
	u_char *p;
	struct identity *identp;

	while (1) {
		/* check interation counter here */
		if (++iterations >= TAC_PLUS_MAX_ITERATIONS) {
			report(LOG_ERR, "%s: %s Too many iterations for choose_authen", session.peer, session.port);
			return (0);
		}
		status = choose_authen(datap, typep);

		if (status && (debug & DEBUG_PACKET_FLAG))
			report(LOG_DEBUG, "choose_authen returns %d", status);

		switch (status) {
			case CHOOSE_BADTYPE: /* FIXME */
			default:
				send_authen_error("choose_authen: unexpected failure return");
				return (0);
			case CHOOSE_OK:
				if (debug & DEBUG_PACKET_FLAG)
					report(LOG_DEBUG, "choose_authen chose %s", typep->authen_name);
				return (1);
			case CHOOSE_FAILED:
				send_authen_error("choose_authen: unacceptable authen method");
				return (0);
			case CHOOSE_GETUSER:
				/*
				 * respond with GETUSER containing an optional message from
				 * authen_data.server_msg.
				 */
				datap->status = TAC_PLUS_AUTHEN_STATUS_GETUSER;
				if (datap->service == TAC_PLUS_AUTHEN_SVC_LOGIN) {
					prompt = cfg_get_host_prompt(datap->NAS_id->NAS_ip);
					if (prompt == NULL && !STREQ(datap->NAS_id->NAS_name, datap->NAS_id->NAS_ip)) {
						prompt = cfg_get_host_prompt(datap->NAS_id->NAS_name);
					}

					if (prompt == NULL) {
						prompt = "\nUser Access Verification\n\nUsername: ";
					}
				} else {
					prompt = "Username: ";
				}
				send_authen_reply(TAC_PLUS_AUTHEN_STATUS_GETUSER, /* status */
						prompt, /* msg */
						strlen(prompt), /* msg_len */
						datap->server_data, datap->server_dlen, 0 /* flags */);

				if (datap->server_data) {
					free(datap->server_data);
					datap->server_dlen = 0;
				}
				/* expect a CONT from the NAS */
				reply = get_authen_continue();
				if (reply == NULL) {
					/* Typically premature close of connection */
					report(LOG_ERR, "%s %s: Null reply packet, expecting CONTINUE", session.peer, session.port);
					return (0);
				}

				cont = (struct authen_cont *) (reply + TAC_PLUS_HDR_SIZE);

				if (cont->flags & TAC_PLUS_CONTINUE_FLAG_ABORT) {
					char buf[65537];
					buf[0] = '\0';
					session.aborted = 1;

					if (cont->user_data_len) {
						/* An abort message exists. Log it */
						p = reply + TAC_PLUS_HDR_SIZE + TAC_AUTHEN_CONT_FIXED_FIELDS_SIZE + cont->user_msg_len;

						memcpy(buf, p, cont->user_data_len);
						buf[cont->user_data_len] = '\0';
					}
					report(LOG_INFO, "%s %s: Login aborted by request -- msg: %s", session.peer, session.port, buf);
					free(reply);
					return (0);
				}

				p = reply + TAC_PLUS_HDR_SIZE + TAC_AUTHEN_CONT_FIXED_FIELDS_SIZE;

				identp = datap->NAS_id;

				if (identp->username) {
					free(identp->username);
				}
				identp->username = tac_make_string(p, cont->user_msg_len);
				free(reply);
		}
	}
	/* NOTREACHED */
}

/*
 * Perform authentication assuming we have successfully chosen an
 * authentication method
 */
static void authenticate(struct authen_data *datap, struct authen_type *typep)
{
	int iterations = 0;
	u_char *reply, *p;
	struct authen_cont *cont;
	int (*func)();

	if (debug & DEBUG_PACKET_FLAG)
		report(LOG_DEBUG, "Calling authentication function");

	func = typep->authen_func;

	if (!func) {
		send_authen_error("authenticate: cannot find function pointer");
		return;
	}

	while (1) {
		if (session.aborted)
			return;

		if (++iterations >= TAC_PLUS_MAX_ITERATIONS) {
			send_authen_error("Too many iterations while authenticating");
			return;
		}

		if ((*func)(datap)) {
			send_authen_error("Unexpected authentication function failure");
			return;
		}

		switch (datap->status) {
			default:
				send_authen_error("Illegal status value from authentication "
						"function");
				return;
			case TAC_PLUS_AUTHEN_STATUS_PASS:
				/* A successful authentication */
				send_authen_reply(TAC_PLUS_AUTHEN_STATUS_PASS, datap->server_msg, datap->server_msg ? strlen(datap->server_msg) : 0,
						datap->server_data, datap->server_dlen, 0);
				return;
			case TAC_PLUS_AUTHEN_STATUS_ERROR:
				/*
				 * never supposed to happen. reply with a server_msg if any, and
				 * bail out
				 */
				send_authen_error(datap->server_msg ? datap->server_msg : "authentication function: unspecified failure");
				return;
			case TAC_PLUS_AUTHEN_STATUS_FAIL:
				/* An invalid user/password combination */
				send_authen_reply(TAC_PLUS_AUTHEN_STATUS_FAIL, datap->server_msg, datap->server_msg ? strlen(datap->server_msg) : 0,
						NULL, 0, 0);
				return;
			case TAC_PLUS_AUTHEN_STATUS_GETUSER:
			case TAC_PLUS_AUTHEN_STATUS_GETPASS:
			case TAC_PLUS_AUTHEN_STATUS_GETDATA:
				/* ship GETPASS/GETDATA containing datap->server_msg to NAS. */
				send_authen_reply(datap->status, datap->server_msg, datap->server_msg ? strlen(datap->server_msg) : 0,
						datap->server_data, datap->server_dlen, datap->flags);

				datap->flags = 0;

				if (datap->server_msg) {
					free(datap->server_msg);
					datap->server_msg = NULL;
				}
				if (datap->server_data) {
					free(datap->server_data);
					datap->server_data = NULL;
				}
				if (datap->client_msg) {
					free(datap->client_msg);
					datap->client_msg = NULL;
				}
				reply = get_authen_continue();
				if (!reply) {
					/* Typically due to a premature connection close */
					report(LOG_ERR, "%s %s: Null reply packet, expecting CONTINUE", session.peer, session.port);

					/* Tell the authentication function it should clean up
					 any private data */

					datap->flags |= TAC_PLUS_CONTINUE_FLAG_ABORT;

					if (datap->method_data)
						((*func)(datap));

					datap->flags = 0;
					return;
				}

				cont = (struct authen_cont *) (reply + TAC_PLUS_HDR_SIZE);

				if (cont->flags & TAC_PLUS_CONTINUE_FLAG_ABORT) {
					session.aborted = 1;

					/* Tell the authentication function to clean up
					 its private data, if there is any */

					datap->flags |= TAC_PLUS_CONTINUE_FLAG_ABORT;
					if (datap->method_data)
						((*func)(datap));
					datap->flags = 0;

					if (cont->user_data_len) {
						/*
						 * An abort message exists. Create a null-terminated
						 * string for authen_data
						 */
						datap->client_data = (char *) tac_malloc(cont->user_data_len + 1);

						p = reply + TAC_PLUS_HDR_SIZE + TAC_AUTHEN_CONT_FIXED_FIELDS_SIZE + cont->user_msg_len;

						memcpy(datap->client_data, p, cont->user_data_len);
						datap->client_data[cont->user_data_len] = '\0';
					}

					free(reply);
					return;
				}

				p = reply + TAC_PLUS_HDR_SIZE + TAC_AUTHEN_CONT_FIXED_FIELDS_SIZE;

				switch (datap->status) {
					case TAC_PLUS_AUTHEN_STATUS_GETDATA:
					case TAC_PLUS_AUTHEN_STATUS_GETPASS:
						/* A response to our GETDATA/GETPASS request. Create a
						 * null-terminated string for authen_data */
						datap->client_msg = (char *) tac_malloc(cont->user_msg_len + 1);
						memcpy(datap->client_msg, p, cont->user_msg_len);
						datap->client_msg[cont->user_msg_len] = '\0';
						free(reply);
						continue;
					case TAC_PLUS_AUTHEN_STATUS_GETUSER:
					default:
						report(LOG_ERR, "%s: authenticate: cannot happen", session.peer);
						send_authen_error("authenticate: cannot happen");
						free(reply);
						return;
				}
		}
		/* NOTREACHED */
	}
}
